package eo.configurator.hibernate;

import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import eo.application.ApplicationConfiguration;
import eo.application.hibernate.HibernateApplicationInstance;
import eo.builder.URLVisitor;
import eo.builder.URLVisitorFactory;
import eo.builder.exception.ApplicationConfigurationException;
import eo.builder.util.ClassListingIterationStep;
import eo.builder.util.XMLReaderHelper;
import eo.descriptor.query.NamedQueryDescriptor;
import eo.descriptor.query.impl.UnmodifiableNamedQueryDescriptor;

/**
 * Configures a hibernate JPA instance.
 * 
 * @author takeshi
 */
public final class HibernateApplicationConfiguration implements
        ApplicationConfiguration, Externalizable {

    private transient List<Class<?>> annotatedMappings = new LinkedList<Class<?>>();
    private transient List<Class<?>> classMappings = new LinkedList<Class<?>>();
    private List<URL> classpath = new ArrayList<URL>();
    private List<URL> configs = new ArrayList<URL>();
    private Map<String, String> configurationProperties = new TreeMap<String, String>();
    private Map<String, String> connectionProperties = new TreeMap<String, String>();
    private String dialect;
    private transient List<String> fileMappings = new LinkedList<String>();
    private transient List<String> jarMappings = new LinkedList<String>();
    private transient Map<String, NamedQueryDescriptor> namedQueries = new HashMap<String, NamedQueryDescriptor>();
    private transient List<String> packageMappings = new LinkedList<String>();
    private transient List<String> resourceMappings = new LinkedList<String>();
    private String url;
    private transient XMLReaderHelper xmlReader = new XMLReaderHelper();

    private String driverClassName;

    public HibernateApplicationConfiguration() {
    }

    public HibernateApplicationConfiguration(final File... files)
            throws SAXException, IOException, ParserConfigurationException,
            ApplicationConfigurationException {
        for (File file : files) {
            this.addConfigurationFile(file.toURI().toURL());
        }
    }

    public HibernateApplicationConfiguration(final String... resources)
            throws ParserConfigurationException, SAXException, IOException,
            ApplicationConfigurationException {
        for (String string : resources) {
            URL url = this.getClass().getClassLoader().getResource(string);
            this.addConfigurationFile(url);
        }
    }

    public HibernateApplicationConfiguration(final URL... urls)
            throws ParserConfigurationException, SAXException, IOException,
            ApplicationConfigurationException {
        for (URL url : urls) {
            this.addConfigurationFile(url);
        }
    }

    public void addConfigurationFile(final URL url) throws IOException,
            ApplicationConfigurationException {
        try {
            this.parseURLReadDocument(url);
            this.configs.add(url);
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException(e);
        } catch (SAXException e) {
            throw new IOException("Illegal file format: " + url);
        }
    }

    private void addNamedQuery(final NamedQuery query) {
        this.namedQueries.put(query.name(),
                new UnmodifiableNamedQueryDescriptor(query.name(), query
                        .query()));
    }

    public HibernateApplicationInstance create()
            throws ApplicationConfigurationException {
        if (this.dialect == null || "".equals(this.dialect)) {
            throw new ApplicationConfigurationException(
                    "configuration.hibernate.dialect.notSet");
        }
        if (this.driverClassName == null || "".equals(this.driverClassName)) {
            throw new ApplicationConfigurationException(
                    "configuration.driver.notSet");
        }
        try {
            Class.forName(this.driverClassName, true, Thread.currentThread()
                    .getContextClassLoader());
        } catch (ClassNotFoundException e) {
            throw new ApplicationConfigurationException("driver.notFound");
        }
        if (this.url == null || "".equals(this.url)) {
            throw new ApplicationConfigurationException(
                    "configuration.url.notSet");
        }
        return new HibernateApplicationInstance(this.connectionProperties,
                this.configurationProperties, this.url, this.namedQueries,
                dialect, driverClassName, this.configs
                        .toArray(new URL[this.configs.size()]));
    }

    private void extractNamedQueries(final Class<?> class1) {
        NamedQuery query = class1.getAnnotation(NamedQuery.class);
        if (query != null) {
            this.addNamedQuery(query);
        }
        NamedQueries queries = class1.getAnnotation(NamedQueries.class);
        if (queries != null) {
            NamedQuery[] nqs = queries.value();
            for (NamedQuery namedQuery : nqs) {
                this.addNamedQuery(namedQuery);
            }
        }
    }

    private Class<?> findClass(final String textContent)
            throws ApplicationConfigurationException {
        Class<?> cl = this.findClassWithClassLoader(textContent, this
                .getClass().getClassLoader());
        if (cl == null) {
            cl = this.findClassWithClassLoader(textContent, Thread
                    .currentThread().getContextClassLoader());
        }
        if (cl == null) {
            throw new ApplicationConfigurationException("classNotFound",
                    textContent);
        }
        return cl;
    }

    private Class<?> findClassWithClassLoader(final String className,
            final ClassLoader loader) {
        try {
            return Class.forName(className, false, loader);
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    public String getConfigurationProperty(final String name) {
        return this.configurationProperties.get(name);
    }

    public String getConnectionProperty(final String name) {
        return this.connectionProperties.get(name);
    }

    /**
     * @return the dialect
     */
    public String getDialect() {
        return this.dialect;
    }

    public String getURL() {
        return this.url;
    }

    private boolean isAnnotatedClass(final Class<?> class1) {
        return class1.isAnnotationPresent(Entity.class)
                || class1.isAnnotationPresent(Embeddable.class)
                || class1.isAnnotationPresent(MappedSuperclass.class);
    }

    private void lookupHbm(final String textContent) throws IOException,
            ParserConfigurationException, SAXException {
        URL resource = this.getClass().getClassLoader().getResource(
                textContent.replaceAll("[.]", "/") + ".hbm.xml");
        if (resource != null) {
            InputStream input = resource.openStream();
            Document doc = this.xmlReader.buildDom(input);
            this.readHbm(doc);
        }
    }

    private void parseClass(final Set<Class<?>> mappedClasses, final Node pname)
            throws IOException, ParserConfigurationException, SAXException,
            ApplicationConfigurationException {
        Class<?> clazz = this.findClass(pname.getTextContent());
        if (this.isAnnotatedClass(clazz)) {
            this.annotatedMappings.add(clazz);
            mappedClasses.add(clazz);
        } else {
            this.lookupHbm(clazz.getName());
            this.classMappings.add(clazz);
        }
    }

    private void parseConfigurationProperty(final String name,
            final String value) {
        if ("dialect".equals(name)) {
            this.dialect = value.trim();
        }
        this.configurationProperties.put(name, value);
    }

    private void parseConnectionProperty(final String propertyName,
            final String propertyValue) {
        if ("connection.url".equals(propertyName)) {
            this.url = propertyValue;
        } else if ("connection.driver_class".equals(propertyName)) {
            this.driverClassName = propertyValue;
        }
        this.connectionProperties.put(propertyName, propertyValue);
    }

    private void parseFile(final Set<Class<?>> mappedClasses,
            final String textContent) throws ParserConfigurationException,
            SAXException, IOException {
        File file = new File(textContent);
        if (file != null && file.exists()) {
            InputStream input = new FileInputStream(file);
            Document doc = this.xmlReader.buildDom(input);
            if (doc != null) {
                this.readHbm(doc);
            }
            this.fileMappings.add(textContent);
        } else {
            throw new FileNotFoundException(file.getAbsolutePath());
        }
    }

    private void readHbm(Document doc) {
        NodeList els = doc.getElementsByTagName("query");
        for (int i = 0; i < els.getLength(); i++) {
            Node node = els.item(i);
            String name = node.getAttributes().getNamedItem("name")
                    .getTextContent();
            String query = node.getTextContent();
            NamedQueryDescriptor desc = new UnmodifiableNamedQueryDescriptor(
                    name, query);
            this.namedQueries.put(name, desc);
        }
    }

    private void parseJar(final Set<Class<?>> mappedClasses, final Node pname)
            throws IOException {
        String val = pname.getTextContent();
        this.jarMappings.add(val);
    }

    private void parsePackage(final Set<Class<?>> mappedClasses,
            final String textContent) {
        this.packageMappings.add(textContent);
    }

    private void parseResource(final Set<Class<?>> mappedClasses,
            final String textContent) throws ParserConfigurationException,
            SAXException, IOException {
        this.resourceMappings.add(textContent);
        InputStream input = this.getClass().getClassLoader()
                .getResourceAsStream(textContent);
        Document doc = this.xmlReader.buildDom(input);
        if (doc != null) {
            this.readHbm(doc);
        }
    }

    protected final void parseURLReadDocument(final URL url)
            throws ParserConfigurationException, SAXException, IOException,
            ApplicationConfigurationException {
        Document doc = this.xmlReader.buildDom(url.openStream());
        this.readDocument(doc);
    }

    private void readDocument(final Document doc) throws IOException,
            DOMException, ParserConfigurationException, SAXException,
            ApplicationConfigurationException {
        NodeList list = doc.getElementsByTagName("property");
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);
            Node propertyName = node.getAttributes().getNamedItem("name");
            String name = propertyName.getTextContent();
            String value = node.getTextContent();
            this.setProperty(name, value);
        }

        list = doc.getElementsByTagName("mapping");
        Set<Class<?>> mappedClasses = new HashSet<Class<?>>();
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);
            NamedNodeMap attributes = node.getAttributes();
            Node pname = attributes.getNamedItem("jar");
            if (pname != null) {
                this.parseJar(mappedClasses, pname);
                continue;
            }
            pname = attributes.getNamedItem("class");
            if (pname != null) {
                this.parseClass(mappedClasses, pname);
                continue;
            }
            pname = attributes.getNamedItem("file");
            if (pname != null) {
                this.parseFile(mappedClasses, pname.getTextContent());
                continue;
            }
            pname = attributes.getNamedItem("resource");
            if (pname != null) {
                this.parseResource(mappedClasses, pname.getTextContent());
                continue;
            }
            pname = attributes.getNamedItem("package");
            if (pname != null) {
                this.parsePackage(mappedClasses, pname.getTextContent());
                continue;
            }
        }
        for (Class<?> class1 : mappedClasses) {
            this.extractNamedQueries(class1);
        }
    }

    protected Collection<? extends Class<?>> scanJarFile(
            final String textContent) throws IOException {
        URL url = this.getClass().getClassLoader().getResource(textContent);
        if (url == null) {
            throw new IllegalArgumentException("No such JAR: " + textContent);
        }
        URLVisitor vis = URLVisitorFactory.getURLVisitor(url);
        ClassListingIterationStep step = new ClassListingIterationStep(this
                .getClass().getClassLoader());
        vis.iterate(step, url);
        Set<Class<?>> classes = step.getClasses();
        Set<Class<?>> mapped = new HashSet<Class<?>>();
        for (Class<?> class1 : classes) {
            if (this.isAnnotatedClass(class1)) {
                mapped.add(class1);
            }
        }
        return mapped;
    }

    public void setClasspathEntries(final URL... urls) {
        for (URL url : urls) {
            this.classpath.add(url);
        }
    }

    public void setProperty(final String name, final String value) {
        if ((name != null) && (name.length() > 0)) {
            if (name.startsWith("connection")) {
                this.parseConnectionProperty(name, value.trim());
            } else {
                this.parseConfigurationProperty(name, value.trim());
            }
        }

    }

    public void setUrl(final String url) {
        this.url = url;
    }

    public Map<String, String> getConfigurationProperties() {
        return new HashMap<String, String>(this.configurationProperties);
    }

    public Map<String, String> getConnectionProperties() {
        return new HashMap<String, String>(this.connectionProperties);
    }

    public void saveToStream(final OutputStream out) throws IOException {
        ObjectOutputStream oout = new ObjectOutputStream(out);
        oout.writeObject(this);
        oout.flush();
    }

    @SuppressWarnings("unchecked")
    public void readExternal(final ObjectInput in) throws IOException,
            ClassNotFoundException {
        this.dialect = in.readObject().toString();
        this.url = in.readObject().toString();
        this.classpath = (List<URL>) in.readObject();
        this.configs = (List<URL>) in.readObject();

        for (URL url : this.configs) {
            try {
                this.parseURLReadDocument(url);
            } catch (ParserConfigurationException e) {
                throw new IllegalStateException(
                        "An error occured while retrieving SAX parser: "
                                + e.getMessage(), e);
            } catch (SAXException e) {
                throw new IOException("Could not parse document from stream: "
                        + e.getCause());
            } catch (ApplicationConfigurationException e) {
                throw new IOException(e.getMessage());
            }
        }
        // overwrite configurations from the config files
        Map<String, String> configs = (Map<String, String>) in.readObject();
        this.configurationProperties.putAll(configs);
        Map<String, String> conns = (Map<String, String>) in.readObject();
        this.connectionProperties.putAll(conns);
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(this.dialect);
        out.writeObject(this.url);
        out.writeObject(this.classpath);
        out.writeObject(this.configs);
        out.writeObject(this.configurationProperties);
        out.writeObject(this.connectionProperties);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((classpath == null) ? 0 : classpath.hashCode());
        result = prime * result + ((configs == null) ? 0 : configs.hashCode());
        result = prime
                * result
                + ((configurationProperties == null) ? 0
                        : configurationProperties.hashCode());
        result = prime
                * result
                + ((connectionProperties == null) ? 0 : connectionProperties
                        .hashCode());
        result = prime * result + ((dialect == null) ? 0 : dialect.hashCode());
        result = prime * result + ((url == null) ? 0 : url.hashCode());
        return result;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final HibernateApplicationConfiguration other = (HibernateApplicationConfiguration) obj;
        if (classpath == null) {
            if (other.classpath != null)
                return false;
        } else if (!classpath.equals(other.classpath))
            return false;
        if (configs == null) {
            if (other.configs != null)
                return false;
        } else if (!configs.equals(other.configs))
            return false;
        if (configurationProperties == null) {
            if (other.configurationProperties != null)
                return false;
        } else if (!configurationProperties
                .equals(other.configurationProperties))
            return false;
        if (connectionProperties == null) {
            if (other.connectionProperties != null)
                return false;
        } else if (!connectionProperties.equals(other.connectionProperties))
            return false;
        if (dialect == null) {
            if (other.dialect != null)
                return false;
        } else if (!dialect.equals(other.dialect))
            return false;
        if (url == null) {
            if (other.url != null)
                return false;
        } else if (!url.equals(other.url))
            return false;
        return true;
    }

    public void setDriverClassName(String className)
            throws ApplicationConfigurationException {
        this.findClass(className);
        this.driverClassName = className;
    }

    public String getDriverClassName() {
        return this.driverClassName;
    }

}
